## Guide (WIP)

<span className="text-muted-foreground font-medium">As of: {new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}</span>

This helps you to get started with the data-table and define the columns, filters, search params, sheet fields and more.

> If you have any questions, feature request please open an issue on [GitHub](https://github.com/openstatusHQ/data-table-filters/issues). This is a work in progress and we are happy to hear from you.

Remember, we are using the shadcn/ui components. Some modifications still live in `src/components/custom/*` but will eventually be migrating back to shadcn/ui to be fully compliant.

The infinite scrollable data-table is mainly composed of the following parts:
- `DataTableFilterControls`: the left sidebar controls
- `DataTableFilterCommand`: the cmdk input
- `DataTableInfinite`: the infinite data-table
- `DataTableDetailsSheet`: the details sheet
- `TimelineChart`: the timeline chart

You will have full control over the data-table and the components that are rendered. Feel free to extend, modify or remove components as you see fit.

## Table

### Table Columns

The `columns.tsx` file is our known source of truth for the columns. If you are familiar with shadcn, you should feel right at home. Check out the shadcn/ui [docs](https://ui.shadcn.com/docs/components/data-table) for more information.

#### Extending Tanstack Table Types

We extend the meta types mainly for _styling_ and _filter functions_. That way, we can avoid changing the data-table components itself.

```tsx
import "@tanstack/react-table";

declare module "@tanstack/react-table" {
  interface TableMeta<TData extends unknown> {
    /**
     * Function to style the row based on values, e.g. color-coding.
     */
    getRowClassName?: (row: Row<TData>) => string;
  }

  interface ColumnMeta {
    /**
     * Class name to style the header cell.
     */
    headerClassName?: string;
    /**
     * Class name to style the cell.
     */
    cellClassName?: string;
    /**
     * Label for the column when neither `id` nor `header` can be used.
     */
    label?: string;
  }

  interface FilterFns {
    /**
     * Filter function to filter the data based on a date range.
     */
    inDateRange?: FilterFn<any>;
    /**
     * Filter function to filter the data based on an array of values.
     */
    arrSome?: FilterFn<any>;
  }

  interface ColumnFiltersOptions<TData extends RowData> {
    filterFns?: Record<string, FilterFn<TData>>;
  }
}
```

### Table Rows

The more data you have, the more you need to think about how to render the rows, especially in an infinitely scrollable data-table.

We currently memoize with a custom deps function to avoid re-rendering the rows when the data is the same.

```tsx
const MemoizedRow = React.memo(
  ({ row, selected }: { row: Row<unknown>; selected: boolean }) => {
    useQueryState("live", searchParamsParser.live);
    return /* ... */
  },
  (prev, next) => prev.row.id === next.row.id && prev.selected === next.selected
) as typeof Row;
```

The row re-renders only if the `id` or `selected` state of the row changes, and with the `useQueryState` hook, we force a re-render when the `live` param changes (making the `getRowClassName` prop being called to set an opacity to the row).

> At a certain point we need to think adding `@tanstack/react-virtual` to the mix.



## Filters

Right now, we need to define the filters in two places:

- `filterFields` in the `constants.tsx` file
- `searchParams` in the `search-params.ts` file

The main reason for this is that we want to keep the `filterFields` as the source of truth for the filters. The `searchParams` are used to filter the data on the server side and to build the search command. Both are synced.

> It would be great if we could merge both values, the `filterFields` and the `searchParams` together and have a single source of truth. - Or make both types extend a single type for better type safety. Hopefully, we can achieve this in the future.

### filterFields

The `filterFields` array within the `constants.tsx` file is what defines the left sidebar controls of the data-table and the cmdk input items.

The _input_, _checkbox_, _slider_ and _timerange_ types are currently supported.

All types extend the `Base` type which defines the following properties:

```ts
export type Base<TData> = {
  /**
   * The label of the filter field.
   */
  label: string;
  /**
   * The value of the filter field - same as the column `id`
   */
  value: keyof TData;
  /**
   * Defines if the accordion in the filter bar is open by default
   */
  defaultOpen?: boolean;
  /**
   * Defines if the command input is disabled for this field
   */
  commandDisabled?: boolean;
};
```

To support the cmdk input, we need to define the `options` property for all types.

```ts
export type Option = {
  label: string;
  value: string | boolean | number | undefined;
};
```

#### Input

For the `input` type, there is nothing to define. 

Within the Controls sidebar, it will be rendered as a simple text input field. The cmdk input will list all the defined `options` once the value is selected.

```ts
export type Input = {
  type: "input";
  options?: Option[];
};
```

**Limitations**: Spaces cannot be used in the input value as they get interpreted as a separator in the cmdk input (and vice versa). An issue [#27](https://github.com/openstatusHQ/data-table-filters/issues/27) is open to tackle this.

#### Checkbox

The `checkbox` type will be rendered the options as list of checkboxes in the Controls sidebar. If there are more than 5 options, a search input will be included on top of the list to filter the options. The cmdk input will list all the defined `options` once the value is selected. You can also define a custom component to be rendered for each option.

```ts
export type Checkbox = {
  type: "checkbox";
  component?: (props: Option) => JSX.Element | null;
  options?: Option[];
};
```

> Currently, only the `checkbox` type has a `component` prop. This is used to render a custom checkbox component.

#### Slider

The `slider` type will be rendered as a slider in the Controls sidebar and have a min and max value input field. The cmdk input will list all the defined `options` once the value is selected.

```ts
export type Slider = {
  type: "slider";
  min: number;
  max: number;
  options?: Option[];
};
```

#### Timerange

The `timerange` type will be rendered as a timerange input field in the Controls sidebar. The **cmdk input does not yet support the timerange input**, please disable the cmdk input for the timerange filter field via `commandDisabled: true`.

A `DatePreset` can be defined to preset the timerange values. It allows you easily select a range of dates (e.g. last 7 days, last 30 days, last 3 months, last year, etc.).

```ts
export type DatePreset = {
  label: string;
  from: Date;
  to: Date;
  shortcut: string;
};

export type Timerange = {
  type: "timerange";
  options?: Option[]; // required for TS
  presets?: DatePreset[];
};
```

### Search Params

The `searchParams` array within the `search-params.ts` file is what defines the search params that are used to filter the data in the data-table.

It mainly includes the same fields as the `filterFields` array - but defined with the `nuqs` parser functions.

**Make sure to have both the `filterFields` and the `searchParams` in sync.**

## Details Sheet

The `sheetFields` array within the `constants.tsx` file is what defines the components inside of the `Sheet` component that is rendered when you select a row in the data-table.

> Yet to be implemented: only trigger a selection if the `sheetFields` is defined.


By default, we will wrap every key-value pair with the `DataTableSheetRowAction` component that will allow you to filter the data-table based on the selected row. Based on the `type`, we will support different filter selections.

> We should use the same types for the `filterFields` and the `sheetFields` to avoid confusion and create a `readonly: boolean` type to avoid that the value is not filterable from the sheet.

We allow you to override the default behavior by providing a custom component.

When loading the sheet and if the data is not available yet, we will show a skeleton component.

```ts
export type SheetField<TData, TMeta = Record<string, unknown>> = {
  /**
   * The id of the field - same as the column `id`
   */
  id: keyof TData;
  /**
   * The label of the field
   */
  label: string;
  /**
   * The type of the field
   */
  type: "readonly" | "input" | "checkbox" | "slider" | "timerange";
  /**
   * The custom component to be rendered
   */
  component?: (
    props: TData & {
      metadata?: {
        totalRows: number;
        filterRows: number;
        totalRowsFetched: number;
      } & TMeta;
    }
  ) => JSX.Element | null | string;
  /**
   * A condition to check if the field should be shown
   */
  condition?: (props: TData) => boolean;
  /**
   * The class name of the field
   */
  className?: string;
  /**
   * The class name of the skeleton
   */
  skeletonClassName?: string;
};
```

## API

We use `@tanstack/react-query` and its `useInfiniteQuery` method to fetch the data.

To share the same search options between the client and the server, we use the same `searchParams` array from the `search-params.ts` file within our `route.ts` file. Based on the filters, we query the mock data and return the results.

It will be up to you to implement the actual API endpoint and query the data from your database.


You API has to return the following data:

```ts
export async function GET(req: Request) {
    // ....
    return Response.json({ data, meta } satisfies {
        data: ColumnSchema[], // TODO: defined ColumnSchema
        meta: InfiniteQueryMeta<LogsMeta>,
        nextCursor: number | null, // timestamp of the last item in the data array
        prevCursor: number | null, // timestamp of the first item in the data array - used for live mode
    })
}
```

We initially started with `size` and `start`, making `size*start` the offset but it has some drawbacks when it comes to live mode. Instead, we use the `cursor` to mark a timestamp and the `direction` to determine the direction of the pagination - `"prev"` for live mode or `"next"` for load more. If you don't need live mode, you can ignore the `prevCursor` and `fetchPreviousPage` (including the `LiveButton` component).

To parse the response, we use [`superjson`](https://github.com/flightcontrolhq/superjson) to serialize and deserialize the data. This is especially useful when using `Date` objects or other non-serializable values.

The `meta` object allows us to return additional data that is not part of the `data` array. That can be data for the chart like `chartData`, or statistics to be used in the `Sheet` component like `currentPercentiles`.

```ts
export type InfiniteQueryMeta<TMeta = Record<string, unknown>> = {
  /**
   * The total number of rows in the database after filtering the timerange
   */
  totalRowCount: number;
  /**
   * The number of rows in the data-table after applying the filters
   */
  filterRowCount: number;
  /**
   * The data for the timeline chart
   */
  chartData: BaseChartSchema[];
  /**
   * The facets for every filter field
   */
  facets: Record<string, FacetMetadataSchema>;
  /**
   * The custom metadata for the data-table
   */
  metadata?: TMeta;
};

type FacetMetadataSchema = {
  rows: { value: any; total: number }[];
  total: number;
  min: number;
  max: number;
};

// as for our example, TMeta is defined as LogsMeta
type LogsMeta = {
  currentPercentiles: Record<Percentile, number>;
};
```

## More

### Timeline Chart

The timeline chart helps you to visualize the data over time. It utilizes the shadcn component (checkout out the [docs](https://ui.shadcn.com/docs/components/chart)) and renders the bars based on the the amount of data items you defined.

```ts
type BaseChartSchema = {
    timestamp: number;
    [key: string]: number; // expects "success", "warning", "error" as keys
}

interface TimelineChartProps<TChart extends BaseChartSchema> = {
    /**
     * The data from `chartData` in the `meta` object
     */
    data: TChart[];
    // ...
}
```

As default, the data keys are coming from the `level` field that takes "**success**", "**warning**", "**error**" as values.

> The `TimelineChart` component is currently not configurable (especially the labels/tooltips) via props but will be in the future. You'll have to update the component yourself to adapt to your needs.

### Live Mode

The live mode is a feature that allows you to see the data in real-time.

We append the `live` param to the search params parser (**not** the filter fields) to make trigger a new fetch every 4 seconds. Thanks to the search params, the live mode retains after a page reload.

The `fetchPreviousPage` from the the `useInfiniteQuery` is being used. The newly fetched data will be appended to the beginning of the data array. (contrary to the `fetchNextPage` which appends to the end of the data array)

For the demo, we add some mock data in the future to see the live mode in action. At some point, no more can be fetcheed anymore. You'll need to refresh the page or wait for the cache to miss. 

## Debugging

We use `react-scan` to for performance debugging. It will show you the components that are being re-rendered. You can enable it by setting the `NEXT_PUBLIC_REACT_SCAN` environment variable to `true` and is automatically enabled in the development environment.

For tanstack query, we use the `ReactQueryDevtools` to see the queries and mutations. It automatically shows up in the development environment.

Tanstack table debug logs can be enabled by setting the `NEXT_PUBLIC_TABLE_DEBUG` environment variable to `true`.

```
# .env.local
NEXT_PUBLIC_REACT_SCAN=true
NEXT_PUBLIC_TABLE_DEBUG=true
```
